/* eslint-disable */

/*
const queue = new Queue({
assetsURL: '',
videosURL: ''
});

// single file
queue.on('file:progress', (e)=>{}); // ~~(e.loaded * 100);
queue.on('file:complete', (e)=>{});   // e.result;
queue.loadFile( src ); or assets.push({ id:'', src:'', type:'' });

// array files
const assets = [];
assets.push( src ); or assets.push({ id:'', src:'', type:'' });
queue.on('progress', (e)=>{}); // ~~(e.loaded * 100);
queue.on('complete', (e)=>{});   // e.getResults()  or e.target.get( id );
queue.load( assets );

// e.target is queue instance
// id is fileName, e.g: e.target.get( 'tmp.mp4' );
e.target.get( id );
e.target.getResults();
e.target.dispose();

// destroy
e.target.destroy();
e.target = null;
*/

const ASSETS_URL = '';
const VIDEOS_URL = '';
const AUDIOS_URL = '';
const AUDIO_METHOD = 'oncanplay'; // oncanplay, oncanplaythrough
const VIDEO_METHOD = 'oncanplay'; // oncanplay, oncanplaythrough
const VIDEO_MUTED = true;
const VIDEO_PLAYSINLINE = true;
const TIMEOUT = 20000;
const BINARY = 'binary'; // e.g 'bin', 'binary', 'glb':
const HTML_SVG = 'svg'; // return <svg> </svg>, or as 'image' <img>
const TEXT = 'text'; // e.g 'frag', 'glsl', 'obj', 'txt'
const CSS = 'css';
const IMAGE = 'image';
const JAVASCRIPT = 'js';
const JSONFILE = 'json'; // e.g 'json', 'gltf'
const AUDIO = 'audio';
const VIDEO = 'video'; // muted, playsinline as default
const XML = 'xml';
const PATTERN = /(\w+:\/{2})?((?:\w+\.){2}\w+)?(\/?[\S]+\/|\/)?([\w\-%\.]+)(?:\.)(\w+)?(\?\S+)?/i;

const value = ( val, defaults ) => val == undefined ? defaults : val;

class Loader {

	constructor() {
		this.loaded = false;
		this.canceled = false;
		this.progress = 0;
		this.item = null;
		this.listeners = null;
	}

	on( _type, _listener ) {
		let listeners = this.listeners;
		if ( !listeners ) { listeners = this.listeners = {}; } else { this.off( _type, _listener ); }
		let arr = listeners[ _type ];
		if ( !arr ) { arr = listeners[ _type ] = []; }
		arr.push( _listener );
		return _listener;
	}

	off( _type, _listener ) {
		const listeners = this.listeners;
		if ( !listeners ) { return; }
		const arr = listeners[ _type ];
		if ( !arr ) { return; }
		for ( let i = 0, l = arr.length; i < l; i++ ) {
			if ( arr[ i ] == _listener ) {
				if ( l == 1 ) { delete( listeners[ _type ] ); } else { arr.splice( i, 1 ); }
				break;
			}
		}
	}

	emit( _event, _target ) {
		let ret = false;
		const listeners = this.listeners;
		if ( _event && listeners ) {

			if ( typeof _event == 'string' ) { _event = { type: _event }; }
			let arr = listeners[ _event.type ];
			if ( !arr ) { return ret; }
			_event.target = _target || this;
			arr = arr.slice();

			for ( let i = 0, l = arr.length; i < l; i++ ) {
				const evt = arr[ i ];
				if ( evt.handleEvent ) { ret = ret || evt.handleEvent( _event ); } else { ret = ret || evt( _event ); }
			}
		}
		return !!ret;
	}

	destroy() {

		const listeners = this.listeners;
		if ( listeners ) {
			for ( const key in listeners ) {
				let arr = listeners[ key ];
				arr.length = 0;
				arr = null;
				delete( listeners[ key ] );
			}
		}
		this.listeners = null;

		// clear Media Caches
		for ( const src in this.caches ) {
			const object = this.caches[ src ];
			let media = null;
			if ( /\.(mp4|webm)$/.test( src ) ) {
				media = object.video;
				media.pause();
				media[ object.method ] = null;
			} else {
				media = object.audio;
				media.pause();
				media[ object.method ] = null;
			}
			if ( media ) {
				if ( media.remove ) media.remove();
				else media.parentNode.removeChild( media );
			}
		}

		this.caches = null;
		if ( this.el != null ) {
			document.body.removeChild( this.el );
			this.el = null;
		}
	}

	getItem() {
		return this.item;
	}

	start() {

		if ( this.isCanceled() ) { return; }
		this.emit( 'loadStart' );

	}

	handle( _value ) {

		// handle progress

		if ( this.isCanceled() ) { return; }

		let event = null;
		if ( typeof ( _value ) == 'number' ) {
			this.progress = _value;
			event = { loaded: this.progress, total: 1 };
		} else {
			event = _value;
			this.progress = _value.loaded / _value.total;
			if ( isNaN( this.progress ) || this.progress == Infinity ) { this.progress = 0; }
		}
		event.target = this;
		event.type = 'progress';

		this.emit( event );
	}

	end() {
		if ( this.isCanceled() ) { return; }
		this.emit( 'complete' );
	}

	isCanceled() {
		if ( this.canceled ) { return true; }
		return false;
	}

	parse( _path ) {
		if ( !_path ) { return null; }
		return _path.match( PATTERN );
	}
}

class XHRLoader extends Loader {

	constructor( _file, _el, _caches ) {

		super();

		let request = null;
		let loadTimeout = null;
		let xhrLevel = 1;
		let response = null;

		this.item = _file;

		this.get = () => response

		this.cancel = () => {
			this.canceled = true;
			clean();
			request.abort();
		}

		this.loadNext = () => {

			request.onloadstart = handleStart;
			request.onprogress = handleProgress;
			request.onabort = handleAbort;
			request.ontimeout = handleTimeout;

			if ( xhrLevel == 1 ) {
				loadTimeout = setTimeout( handleTimeout, TIMEOUT );
			}

			request.onload = handleLoad;
			if ( request.onreadystatechange ) {
				request.onreadystatechange = handleReadyStateChange( this );
			}

			request.send();
		}

		const handleProgress = ( _event ) => {

			if ( _event == undefined ) {
				_event = { loaded: 0, total: 1 }
			}

			if ( _event.loaded > 0 && _event.total == 0 ) {
				return;
			}

			this.handle( {
				loaded: _event.loaded,
				total: _event.total
			} );
		}

		const handleStart = () => {

			clearTimeout( loadTimeout );
			this.start();
		}

		const handleAbort = () => {
			clean();
		}

		const handleReadyStateChange = () => {

			if ( request.readyState == 4 ) {
				handleLoad();
			}
		}

		const handleLoad = () => {

			if ( this.loaded ) { return; }
			this.loaded = true;
			response = getResponse();
			clean();

			const isComplete = generateTag();
			if ( isComplete ) {
				this.end();
			}
		}

		const handleTimeout = () => {
			clean();
		}

		const handleTagReady = () => {
			this.end();
		}

		const getResponse = () => {
			if ( response != null ) {
				return response;
			}

			if ( request.response != null ) {
				return request.response;
			}

			try {
				if ( request.responseText != null ) {
					return request.responseText;
				}
			} catch ( e ) {}

			try {
				if ( request.responseXML != null ) {
					return request.responseXML;
				}
			} catch ( e ) {}
			return null;
		}

		const createXHR = () => {

			const target = document.createElement( 'a' );
			target.href = this.item.src;
			const host = document.createElement( 'a' );
			host.href = location.href;
			const crossDomain = ( target.hostname != '' ) && ( target.port != host.port || target.protocol != host.protocol || target.hostname != host.hostname );

			let req = null;
			if ( crossDomain && window.XDomainRequest ) {
				req = new window.XDomainRequest();
			} else if ( window.XMLHttpRequest ) {
				req = new window.XMLHttpRequest();
			} else {
				try {
					req = new window.ActiveXObject( 'Msxml2.XMLHTTP.6.0' );
				} catch ( e ) {
					try {
						req = new window.ActiveXObject( 'Msxml2.XMLHTTP.3.0' );
					} catch ( e ) {
						try {
							req = new window.ActiveXObject( 'Msxml2.XMLHTTP' );
						} catch ( e ) {
							return false;
						}
					}
				}
			}

			// crossOrigin
			req.crossOrigin = 'anonymous';

			if ( this.item.type == TEXT && req.overrideMimeType ) {
				req.overrideMimeType( 'text/plain; charset=utf-8' );
			}

			xhrLevel = ( typeof req.responseType === 'string' ) ? 2 : 1;

			req.open( 'GET', this.item.src, true );
			if ( crossDomain && req instanceof XMLHttpRequest && xhrLevel == 1 ) {
				req.setRequestHeader( 'Origin', location.origin );
			}

			if ( this.item.type == IMAGE || this.item.type == BINARY ) {
				req.responseType = 'arraybuffer';
			}

			request = req;
			return true;
		}

		const clean = () => {

			clearTimeout( loadTimeout );

			const req = request;
			req.onloadstart = null;
			req.onprogress = null;
			req.onabort = null;
			req.onload = null;
			req.ontimeout = null;
			req.onloadend = null;
			req.onreadystatechange = null;
		}

		const generateTag = () => {

			const type = this.item.type;
			let tag = this.item.tag;
			let head, xml;

			switch ( type ) {

				case IMAGE:
					tag.onload = handleTagReady;
					tag.src = this.item.src;
					tag.crossOrigin = 'anonymous';
					response = tag;
					return false;

				case AUDIO:
					console.warn( "Preloading audio might cause problems.." );
					tag[ this.item.audiosMethod ] = handleTagReady;
					tag.src = this.item.src;
					tag.crossOrigin = 'anonymous';
					tag.autoplay = false;
					tag.loop = true;

					_el.appendChild( tag );
					_caches[ tag.src ] = {
						audio: tag,
						method: this.item.audiosMethod
					}

					response = tag;
					return false;

				case VIDEO:

					const onVideoReady = () => {
						tag[ this.item.videosMethod ] = null;
						handleTagReady();
					};

					// playsinline, muted
					tag.src = this.item.src;
					tag.crossOrigin = 'anonymous';
					tag.autoplay = false;
					tag.loop = true;
					if ( this.item.videosMuted ) {
						tag.muted = true;
					}
					if ( this.item.videosPlaysinline ) {
						tag.setAttribute( 'playsinline', 'playsinline' );
					}
					tag.preload = 'metadata';

					if ( tag.readyState > 3 ) onVideoReady();
					else tag[ this.item.videosMethod ] = onVideoReady;

					_el.appendChild( tag );
					_caches[ tag.src ] = {
						video: tag,
						method: this.item.videosMethod
					}
					tag.load();

					response = tag;
					return false;

				case JAVASCRIPT:

					tag.text = response;
					// head = document.getElementsByTagName('head')[0];
					// head.appendChild(tag);

					response = tag;
					return true;

				case CSS:

					if ( tag.styleSheet ) {
						tag.styleSheet.cssText = response;
					} else {
						const textNode = document.createTextNode( response );
						tag.appendChild( textNode );
					}

					// head = document.getElementsByTagName('head')[0];
					// head.appendChild(tag);

					response = tag;
					return true;

				case XML:
					xml = parseXML( response, 'text/xml' );
					response = xml;
					return true;

				case HTML_SVG:
					xml = parseXML( response, 'image/svg+xml' );
					tag = xml.documentElement;
					response = tag;
					return true;

				case BINARY:
					// return a raw arraybuffer
					response = response;
					return true;

				case JSONFILE:
					let json = {};
					json = window.JSON.parse( response );
					response = json;
					return true;
			}
			return true;
		}

		const parseXML = ( _text, _type ) => {

			let xml = null;
			if ( window.DOMParser ) {
				const parser = new DOMParser();
				xml = parser.parseFromString( _text, _type );
			} else {
				xml = new window.ActiveXObject( 'Microsoft.XMLDOM' );
				xml.async = false;
				xml.loadXML( _text );
			}
			return xml;
		}


		createXHR();
	}
}

class Queue extends Loader {

	constructor( opts = {} ) {

		super();

		const maxConnections = value( opts.maxConnections, 5 );
		const assetsURL = value( opts.assetsURL, ASSETS_URL );
		const videosURL = value( opts.videosURL, VIDEOS_URL );
		const audiosURL = value( opts.audiosURL, AUDIOS_URL );
		const videosMethod = value( opts.videosMethod, VIDEO_METHOD );
		const videosMuted = value( opts.videosMuted, VIDEO_MUTED );
		const videosPlaysinline = value( opts.videosPlaysinline, VIDEO_PLAYSINLINE );
		const audiosMethod = value( opts.audiosMethod, AUDIO_METHOD );

		let alreadyDispatched = false;
		let numItems = 0;
		let numItemsLoaded = 0;

		const next = null;
		const onFileLoad = null;
		const onFileProgress = null;
		const currentLoads = [];
		const loadQueue = [];
		const loadItemsById = {};
		const loadedResults = {};
		const typeCallbacks = {};
		const extensionCallbacks = {};

		// dummy element
		this.el = document.createElement( 'div' );
		this.el.style.display = 'none';
		document.body.appendChild( this.el );
		this.caches = {};

		this.loadFile = _file => {
			addItem( _file );
			loadNext();
		}

		this.load = _manifest => {

			let data = null;

			if ( _manifest instanceof Array ) {
				data = _manifest;
			} else {
				data = [ _manifest ];
			}
			for ( let i = 0, l = data.length; i < l; i++ ) {
				addItem( data[ i ] );
			}

			loadNext();
		}

		this.loadNext = () => {
			loadNext();
		}

		this.getItem = _value => loadItemsById[ _value ]

		this.get = _value => {

			const file = loadItemsById[ _value ];
			if ( file == null ) { return null; }
			return loadedResults[ file.id ];
		}

		this.getResults = _value => loadedResults

		this.dispose = () => {

			for ( const fileId in loadItemsById ) {
				disposeItem( loadItemsById[ fileId ] );
			}
			while ( currentLoads.length ) {
				currentLoads.pop().cancel();
			}

			alreadyDispatched = false;
			this.listeners = null;
		}


		const addItem = ( _value ) => {

			const file = createLoadItem( _value );
			if ( file == null ) { return; }
			const loader = createLoader( file );
			if ( loader != null ) {
				loadQueue.push( loader );
				numItems++;
				updateProgress();
			}
		}

		const createLoadItem = ( _value ) => {

			const file = {};
			const isStr = ( typeof _value == 'string' ) ? true : false;
			const source = ( isStr ? _value : _value.src ).replace( videosURL, '' ).replace( audiosURL, '' ).replace( assetsURL, '' );

			if ( /\.(mp4|webm)$/.test( source ) ) {
				file.src = videosURL + source;
				file.videosMethod = videosMethod;
				file.videosMuted = videosMuted;
				file.videosPlaysinline = videosPlaysinline;

			} else if ( /\.(mp3|wav|ogg)$/.test( source ) ) {

				file.src = audiosURL + source;
				file.audiosMethod = audiosMethod;

			} else {
				file.src = assetsURL + source;
			}

			file.type = isStr ? null : ( _value.type || null );
			file.id = isStr ? null : ( _value.id || null );
			file.tag = isStr ? null : ( _value.tag || null );

			let match = this.parse( file.src );
			if ( match != null ) { file.ext = match[ 5 ]; }
			if ( file.type == null ) {
				file.type = getType( file.ext );
			}

			if ( file.tag == null ) {
				file.tag = generateTag( file.type );
			}

			if ( file.id == null || file.id == '' ) {

				const fileName = file.src.match( /[^\\/]+\.[^\\/]+$/ )[ 0 ];

				// filter out the path, only fileName for file.id

				file.id = fileName;
			}

			const customHandler = typeCallbacks[ file.type ] || extensionCallbacks[ file.ext ];
			if ( customHandler ) {

				const result = customHandler( file.src, file.type, file.id, file.data );

				if ( result === false ) {
					return null;

				} else if ( result === true ) {

				} else {


					if ( result.src != null ) { file.src = result.src; }
					if ( result.id != null ) { file.id = result.id; }
					if ( result.tag != null && result.tag.loadNext instanceof Function ) {
						file.tag = result.tag;
					}
					if ( result.completeHandler != null ) { file.completeHandler = result.completeHandler; }
				}

				if ( result.type ) { file.type = result.type; }

				match = this.parse( file.src );
				if ( match != null ) { file.ext = match[ 5 ]; }
			}

			loadItemsById[ file.id ] = file;
			return file;
		}

		const createLoader = ( _file ) => {

			return new XHRLoader( _file, this.el, this.caches );
		}

		const loadNext = () => {

			if ( !alreadyDispatched ) {
				this.start();
				alreadyDispatched = true;
			}

			if ( numItems == numItemsLoaded ) {
				this.loaded = true;
				this.end();
				if ( next && next.loadNext ) {
					next.loadNext();
				}
			}

			for ( let i = 0, l = loadQueue.length; i < l; i++ ) {

				if ( currentLoads.length >= maxConnections ) { break; }
				const loader = loadQueue[ i ];
				loadQueue.splice( i, 1 );
				loadItem( loader );
				i--;
				l--;
			}
		}

		const loadItem = ( _loader ) => {
			_loader.on( 'progress', handleProgress );
			_loader.on( 'complete', handleComplete );
			currentLoads.push( _loader );
			_loader.loadNext();
		}

		const handleComplete = ( { target } ) => {

			const loader = target;
			const file = loader.getItem();

			loadedResults[ file.id ] = loader.get();
			removeLoadItem( loader );
			finishedLoad( file );

			loader.off( 'progress', handleProgress );
			loader.off( 'complete', handleComplete );
		}

		const finishedLoad = ( _file ) => {
			numItemsLoaded++;

			updateProgress();
			fileComplete( _file );

			loadNext();
		}

		const removeLoadItem = ( _loader ) => {
			const l = currentLoads.length;
			for ( let i = 0; i < l; i++ ) {
				if ( currentLoads[ i ] == _loader ) {
					currentLoads.splice( i, 1 );
					break;
				}
			}
		}

		const handleProgress = ( { target } ) => {

			const loader = target;
			fileProgress( loader.getItem(), loader.progress );
			updateProgress();
		}

		const updateProgress = () => {
			let loaded = numItemsLoaded / numItems;
			const remaining = numItems - numItemsLoaded;
			if ( remaining > 0 ) {
				let chunk = 0;
				for ( let i = 0, l = currentLoads.length; i < l; i++ ) {
					chunk += currentLoads[ i ].progress;
				}
				loaded += ( chunk / remaining ) * ( remaining / numItems );
			}
			this.handle( loaded );
		}

		const disposeItem = ( { id } ) => {

			delete loadedResults[ id ];
			delete loadItemsById[ id ];
		}

		const generateTag = ( _type ) => {

			let tag = null;
			switch ( _type ) {
				case IMAGE:
					return document.createElement( 'img' );
				case AUDIO:
					tag = document.createElement( 'audio' );
					return tag;
				case VIDEO:
					tag = document.createElement( 'video' );
					return tag;
				case JAVASCRIPT:
					tag = document.createElement( 'script' );
					tag.type = 'text/javascript';
					return tag;
				case CSS:
					tag = document.createElement( 'style' );
					tag.rel = 'stylesheet';
					tag.type = 'text/css';
					return tag;
				case HTML_SVG:
					tag = document.createElement( 'svg' );
					return tag;
			}
			return null;
		}

		const getType = ( _extension ) => {

			switch ( _extension ) {
				case 'jpeg':
				case 'jpg':
				case 'gif':
				case 'png':
				case 'webp':
				case 'bmp':
					return IMAGE;
				case 'bin':
				case 'binary':
				case 'pack':
				case 'glb':
					return BINARY;
				case 'ogg':
				case 'mp3':
				case 'wav':
					return AUDIO;
				case 'mp4':
				case 'webm':
					return VIDEO;
				case 'json':
				case 'gltf':
					return JSONFILE;
				case 'xml':
					return XML;
				case 'css':
					return CSS;
				case 'js':
					return JAVASCRIPT;
				case 'svg':
					return HTML_SVG;
				case 'frag':
				case 'vert':
				case 'fs':
				case 'vs':
				case 'glsl':
				case 'obj':
				case 'txt':
				case 'text':
				default:
					return TEXT;
			}
		}

		const fileProgress = ( _file, _progress ) => {

			if ( this.isCanceled() ) { return; }

			const event = {
				target: this,
				type: 'file:progress',
				progress: _progress,
				loaded: _progress,
				total: 1,
				item: _file
			};

			onFileProgress && onFileProgress( event );
			this.emit( event );
		}

		const fileComplete = ( _file ) => {

			if ( this.isCanceled() ) { return; }

			const event = {
				target: this,
				type: 'file:complete',
				item: _file,
				result: loadedResults[ _file.id ]
			};


			if ( _file.completeHandler ) {
				_file.completeHandler( event );
			}

			onFileLoad && onFileLoad( event );
			this.emit( event );
		}
	}
}

export default Queue;